﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ExtensionLoader;
using ExtensionLoader.Config;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using CableBeachMessages;
using OpenSimDbLinq;

namespace WorldServer.Extensions
{
    public class OpenSimAttributes : IExtension<WorldServer>, IAttributeProvider
    {
        const string ATTRIBUTES_FILE = WorldServer.DATA_DIR + "WorldServer.OpenSimAttributes.txt";

        WorldServer server;
        Dictionary<Uri, Dictionary<Uri, OSD>> attributeStorage = new Dictionary<Uri, Dictionary<Uri, OSD>>();
        Dictionary<UUID, Uri> uuidMap = new Dictionary<UUID, Uri>();
        OpenSimDatabase userDatabase;
        Dictionary<Uri, UUID> identityToUUID = new Dictionary<Uri, UUID>();

        public OpenSimAttributes()
        {
        }

        public bool Start(WorldServer server)
        {
            this.server = server;

            try
            {
                OSDMap map = OSDParser.DeserializeJson(new FileStream(ATTRIBUTES_FILE, FileMode.Open, FileAccess.Read)) as OSDMap;
                if (map != null) Deserialize(map);
            }
            catch (Exception) { }

            #region Config Loading

            try
            {
                IConfig userdbConfig = server.ConfigFile.Configs["OpenSimUserDatabase"];
                userDatabase = OpenSimUtils.LoadDatabaseFromConfig(userdbConfig);

                try
                {
                    // DEBUG:
                    //userDatabase.Log = Console.Out;

                    // Run a query to select the the first user in the database to confirm
                    // that database access is working
                    var creationDateQuery = from user in userDatabase.Users select user;
                    var result = creationDateQuery.SingleOrDefault();

                    if (result != null)
                        Logger.Info("[OpenSimAttributes] Connected to user database");
                    else
                        Logger.Warn("[OpenSimAttributes] Connected to an empty user database");
                }
                catch (Exception ex)
                {
                    Logger.Error("[OpenSimAttributes] Failed to establish a database connection: " + ex.Message);
                    return false;
                }
            }
            catch (Exception)
            {
                Logger.Error("[OpenSimAttributes] Failed to load [OpenSimDatabase] section from " + WorldServer.CONFIG_FILE);
                return false;
            }

            #endregion Config Loading

            return true;
        }

        public void Stop()
        {
            if (userDatabase != null)
                userDatabase.Dispose();

            StringBuilder sb = new StringBuilder();
            LitJson.JsonWriter writer = new LitJson.JsonWriter(sb);
            writer.IndentValue = 4;
            writer.PrettyPrint = true;
            OSDParser.SerializeJsonString(Serialize(), ref writer);

            File.WriteAllText(ATTRIBUTES_FILE, sb.ToString());
        }

        public Uri UUIDToIdentity(UUID avatarID)
        {
            Uri identity = null;
            
            if (!uuidMap.TryGetValue(avatarID, out identity))
            {
                // Try looking up this UUID in the agent database
                lock (userDatabase)
                {
                    var query = from user in userDatabase.Users where user.UUID == avatarID.ToString() select new { user.UserName, user.LastName };
                    var result = query.SingleOrDefault();

                    if (result != null)
                        return NameToLocalIdentity(result.UserName, result.LastName);
                }
            }

            return identity;
        }

        public Dictionary<Uri, OSD> GetAttributes(Uri identity)
        {
            if (identity == null)
                return null;

            Dictionary<Uri, OSD> attributes;
            if (!attributeStorage.TryGetValue(identity, out attributes))
            {
                Logger.Debug("[OpenSimAttributes] Creating new attributes for " + identity);
                attributes = new Dictionary<Uri, OSD>();
                attributes[AvatarAttributes.AVATAR_ID] = OSD.FromUUID(IdentityToUUID(identity));
            }

            UUID avatarID = IdentityToUUID(identity);
            if (avatarID != UUID.Zero)
            {
                string avatarIDStr = avatarID.ToString();
                Users dbUser = userDatabase.Users.SingleOrDefault(user => user.UUID == avatarIDStr);

                if (dbUser != null)
                {
                    Logger.Debug("[OpenSimAttributes] Pulling OpenSim user database attributes for " + identity);
                    DatabaseRowToAttributes(dbUser, ref attributes, out avatarID);
                }
            }

            return attributes;
        }

        public void UpdateAttributes(Uri identity, Dictionary<Uri, OSD> attributes)
        {
            lock (attributeStorage)
            {
                attributeStorage[identity] = attributes;
                uuidMap[IdentityToUUID(identity)] = identity;
            }
        }

        #region Persistence

        OSDMap Serialize()
        {
            lock (attributeStorage)
            {
                OSDMap map = new OSDMap();

                foreach (KeyValuePair<Uri, Dictionary<Uri, OSD>> entry in attributeStorage)
                {
                    OSDMap attributes = new OSDMap(entry.Value.Count);

                    foreach (KeyValuePair<Uri, OSD> attributeEntry in entry.Value)
                        attributes.Add(attributeEntry.Key.ToString(), attributeEntry.Value);

                    map.Add(entry.Key.ToString(), attributes);
                }

                return map;
            }
        }

        void Deserialize(OSDMap map)
        {
            int attributeCount = 0;

            lock (attributeStorage)
            {
                attributeStorage.Clear();

                foreach (KeyValuePair<string, OSDMap> entry in map)
                {
                    Dictionary<Uri, OSD> attributes = new Dictionary<Uri, OSD>(entry.Value.Count);

                    foreach (KeyValuePair<string, OSD> attributeEntry in entry.Value)
                        attributes.Add(new Uri(attributeEntry.Key), attributeEntry.Value);

                    Uri identity = new Uri(entry.Key);
                    attributeStorage.Add(identity, attributes);
                    uuidMap[IdentityToUUID(identity)] = identity;
                    ++attributeCount;
                }
            }

            Logger.Info("[OpenSimAttributes] Loaded " + attributeCount + " attributes across " + attributeStorage.Count + " identities");
        }

        #endregion Persistence

        private Uri NameToLocalIdentity(string firstName, string lastName)
        {
            return new Uri(server.HttpUri, "/users/" + firstName + "." + lastName);
        }

        private UUID IdentityToUUID(Uri identity)
        {
            UUID uuid;

            // Cache check
            if (identityToUUID.TryGetValue(identity, out uuid))
                return uuid;

            if (identity.Host == server.HttpUri.Host && identity.Segments.Length == 3)
            {
                // Identity came from the same host this service is running on, try to parse a first and last name out
                string[] firstLast = identity.Segments[identity.Segments.Length - 1].Split('.');

                lock (userDatabase)
                {
                    var query = from user in userDatabase.Users where user.UserName == firstLast[0] && user.LastName == firstLast[1] select user.UUID;
                    string uuidStr = query.SingleOrDefault();

                    if (!String.IsNullOrEmpty(uuidStr))
                    {
                        uuid = UUID.Parse(uuidStr);
                        Logger.Info("[OpenSimAttributes] Resolved local identity " + identity + " to avatarID " + uuid);
                        identityToUUID[identity] = uuid;
                        return uuid;
                    }
                }
            }

            return CableBeachUtils.IdentityToUUID(identity);
        }

        void DatabaseRowToAttributes(Users dbUser, ref Dictionary<Uri, OSD> attributes, out UUID avatarID)
        {
            UUID.TryParse(dbUser.UUID, out avatarID);
            Vector3 homeLookAt = new Vector3(dbUser.HomeLookAtX, dbUser.HomeLookAtY, dbUser.HomeLookAtZ);
            Vector3 homePosition = new Vector3(dbUser.HomeLocationX, dbUser.HomeLocationY, dbUser.HomeLocationZ);
            uint homeRegionX, homeRegionY;
            Utils.LongToUInts((ulong)dbUser.HomeRegion, out homeRegionX, out homeRegionY);
            homeRegionX /= 256;
            homeRegionY /= 256;

            attributes[AvatarAttributes.AVATAR_ID] = OSD.FromUUID(avatarID);

            if (!attributes.ContainsKey(AvatarAttributes.BIOGRAPHY)) attributes[AvatarAttributes.BIOGRAPHY] = OSD.FromString(dbUser.ProfileAboutText);
            if (!attributes.ContainsKey(AvatarAttributes.BIRTH_DATE)) attributes[AvatarAttributes.BIRTH_DATE] = OSD.FromDate(Utils.UnixTimeToDateTime(dbUser.Created));
            if (!attributes.ContainsKey(AvatarAttributes.CAN_DO)) attributes[AvatarAttributes.CAN_DO] = OSD.FromUInteger(dbUser.ProfileCanDoMask);
            if (!attributes.ContainsKey(AvatarAttributes.CUSTOM_TYPE)) attributes[AvatarAttributes.CUSTOM_TYPE] = OSD.FromString(dbUser.CustomType);
            if (!attributes.ContainsKey(AvatarAttributes.EMAIL)) attributes[AvatarAttributes.EMAIL] = OSD.FromString(dbUser.Email);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_BIOGRAPHY)) attributes[AvatarAttributes.FIRST_LIFE_BIOGRAPHY] = OSD.FromString(dbUser.ProfileFirstText);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_IMAGE_ID)) attributes[AvatarAttributes.FIRST_LIFE_IMAGE_ID] = OSD.FromString(dbUser.ProfileFirstImage);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME)) attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(dbUser.UserName);
            if (!attributes.ContainsKey(AvatarAttributes.GOD_LEVEL)) attributes[AvatarAttributes.GOD_LEVEL] = OSD.FromInteger(dbUser.GodLevel);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_LOOKAT)) attributes[AvatarAttributes.HOME_LOOKAT] = OSD.FromVector3(homeLookAt);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_POSITION)) attributes[AvatarAttributes.HOME_POSITION] = OSD.FromVector3(homePosition);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_ID)) attributes[AvatarAttributes.HOME_REGION_ID] = OSD.FromString(dbUser.HomeRegionID);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_X)) attributes[AvatarAttributes.HOME_REGION_X] = OSD.FromUInteger(homeRegionX);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_Y)) attributes[AvatarAttributes.HOME_REGION_Y] = OSD.FromUInteger(homeRegionY);
            if (!attributes.ContainsKey(AvatarAttributes.IMAGE_ID)) attributes[AvatarAttributes.IMAGE_ID] = OSD.FromString(dbUser.ProfileImage);
            if (!attributes.ContainsKey(AvatarAttributes.LAST_NAME)) attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(dbUser.LastName);
            if (!attributes.ContainsKey(AvatarAttributes.PARTNER_ID)) attributes[AvatarAttributes.PARTNER_ID] = OSD.FromString(dbUser.Partner);
            if (!attributes.ContainsKey(AvatarAttributes.USER_FLAGS)) attributes[AvatarAttributes.USER_FLAGS] = OSD.FromInteger(dbUser.UserFlags);
            if (!attributes.ContainsKey(AvatarAttributes.WANT_DO)) attributes[AvatarAttributes.WANT_DO] = OSD.FromInteger(dbUser.ProfileWantDoMask);

            // Fetch the appearance for this avatar
            AvatarAppearance dbAppearance = userDatabase.AvatarAppearance.SingleOrDefault(user => user.Owner == dbUser.UUID);
            if (dbAppearance != null)
            {
                if (!attributes.ContainsKey(AvatarAttributes.EYES_ITEM)) attributes[AvatarAttributes.EYES_ITEM] = OSD.FromString(dbAppearance.EyesItem);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ITEM)) attributes[AvatarAttributes.GLOVES_ITEM] = OSD.FromString(dbAppearance.GlovesItem);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ITEM)) attributes[AvatarAttributes.HAIR_ITEM] = OSD.FromString(dbAppearance.HairItem);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ITEM)) attributes[AvatarAttributes.JACKET_ITEM] = OSD.FromString(dbAppearance.JacketItem);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ITEM)) attributes[AvatarAttributes.PANTS_ITEM] = OSD.FromString(dbAppearance.PantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ITEM)) attributes[AvatarAttributes.SHAPE_ITEM] = OSD.FromString(dbAppearance.BodyItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ITEM)) attributes[AvatarAttributes.SHIRT_ITEM] = OSD.FromString(dbAppearance.ShirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ITEM)) attributes[AvatarAttributes.SHOES_ITEM] = OSD.FromString(dbAppearance.ShoesItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ITEM)) attributes[AvatarAttributes.SKIN_ITEM] = OSD.FromString(dbAppearance.SkinItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ITEM)) attributes[AvatarAttributes.SKIRT_ITEM] = OSD.FromString(dbAppearance.SkirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ITEM)) attributes[AvatarAttributes.SOCKS_ITEM] = OSD.FromString(dbAppearance.SocksItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ITEM)) attributes[AvatarAttributes.UNDERPANTS_ITEM] = OSD.FromString(dbAppearance.UnderpantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ITEM)) attributes[AvatarAttributes.UNDERSHIRT_ITEM] = OSD.FromString(dbAppearance.UndershirtItem);

                if (!attributes.ContainsKey(AvatarAttributes.EYES_ASSET)) attributes[AvatarAttributes.EYES_ASSET] = OSD.FromString(dbAppearance.EyesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ASSET)) attributes[AvatarAttributes.GLOVES_ASSET] = OSD.FromString(dbAppearance.GlovesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ASSET)) attributes[AvatarAttributes.HAIR_ASSET] = OSD.FromString(dbAppearance.HairAsset);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ASSET)) attributes[AvatarAttributes.JACKET_ASSET] = OSD.FromString(dbAppearance.JacketAsset);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ASSET)) attributes[AvatarAttributes.PANTS_ASSET] = OSD.FromString(dbAppearance.PantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ASSET)) attributes[AvatarAttributes.SHAPE_ASSET] = OSD.FromString(dbAppearance.BodyAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ASSET)) attributes[AvatarAttributes.SHIRT_ASSET] = OSD.FromString(dbAppearance.ShirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ASSET)) attributes[AvatarAttributes.SHOES_ASSET] = OSD.FromString(dbAppearance.ShoesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ASSET)) attributes[AvatarAttributes.SKIN_ASSET] = OSD.FromString(dbAppearance.SkinAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ASSET)) attributes[AvatarAttributes.SKIRT_ASSET] = OSD.FromString(dbAppearance.SkirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ASSET)) attributes[AvatarAttributes.SOCKS_ASSET] = OSD.FromString(dbAppearance.SocksAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ASSET)) attributes[AvatarAttributes.UNDERPANTS_ASSET] = OSD.FromString(dbAppearance.UnderpantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ASSET)) attributes[AvatarAttributes.UNDERSHIRT_ASSET] = OSD.FromString(dbAppearance.UndershirtAsset);

                if (!attributes.ContainsKey(AvatarAttributes.TEXTURE_ENTRY)) attributes[AvatarAttributes.TEXTURE_ENTRY] = OSD.FromBinary(dbAppearance.Texture);
                if (!attributes.ContainsKey(AvatarAttributes.VISUAL_PARAMS)) attributes[AvatarAttributes.VISUAL_PARAMS] = OSD.FromBinary(dbAppearance.VisualParams);
                if (!attributes.ContainsKey(AvatarAttributes.AVATAR_HEIGHT)) attributes[AvatarAttributes.AVATAR_HEIGHT] = OSD.FromReal(dbAppearance.AvatarHeight);
            }

            // Fetch the last starting location for this avatar
            Agents dbAgent = userDatabase.Agents.SingleOrDefault(user => user.UUID == dbUser.UUID);
            if (dbAgent != null)
            {
                Vector3 position, lookAt;
                if (!Vector3.TryParse(dbAgent.CurrentPos, out position))
                    position = new Vector3(128f, 128f, 100f);
                if (!Vector3.TryParse(dbAgent.CurrentLookAt, out lookAt))
                    lookAt = Vector3.UnitZ;

                uint regionX, regionY;
                Utils.LongToUInts((ulong)dbAgent.CurrentHandle, out regionX, out regionY);
                regionX /= 256;
                regionY /= 256;

                if (!attributes.ContainsKey(AvatarAttributes.LAST_LOOKAT)) attributes[AvatarAttributes.LAST_LOOKAT] = OSD.FromVector3(lookAt);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_POSITION)) attributes[AvatarAttributes.LAST_POSITION] = OSD.FromVector3(position);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_ID)) attributes[AvatarAttributes.LAST_REGION_ID] = OSD.FromString(dbAgent.CurrentRegion);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_X)) attributes[AvatarAttributes.LAST_REGION_X] = OSD.FromUInteger(regionX);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_Y)) attributes[AvatarAttributes.LAST_REGION_Y] = OSD.FromUInteger(regionY);
            }
        }
    }
}
